﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Database.Core.Attributes;
using Database.Core.Exceptions;

namespace Database.Core
{
  internal class PopulateEntity
  {
    /// <summary>
    ///   Reads a <see cref="DataRow" /> into an existing entity.
    /// </summary>
    /// <typeparam name="T">Entity type.</typeparam>
    /// <param name="entity">Entity to be populated.</param>
    /// <param name="dataRow">The <see cref="DataRow" />.</param>
    internal static void ToEntity<T>(T entity, DataRow dataRow)
    {
      var columns = dataRow.Table.Columns.Cast<DataColumn>().ToList();

      //iterate through columns and set properties of the entity with the matching column values
      Populate(dataRow, entity, columns);
    }

    /// <summary>
    ///   Reads specified <see cref="DataRow" /> into an existing entity.
    /// </summary>
    /// <typeparam name="T">Entity type.</typeparam>
    /// <param name="entity">Entity to be populated.</param>
    /// <param name="dataTable">The <see cref="DataTable" /> containing specified row.</param>
    /// <param name="rowIndex">Index of the row.</param>
    internal static void ToEntity<T>(T entity, DataTable dataTable, int rowIndex)
    {
      var columns = dataTable.Columns.Cast<DataColumn>().ToList();

      //iterate through columns and set properties of the entity with the matching column values
      Populate(dataTable.Rows[rowIndex], entity, columns);
    }

    /// <summary>
    ///   Reads <see cref="DataTable" /> into an <see cref="IEnumerable" /> of the specified entity type.
    /// </summary>
    /// <typeparam name="T">The type of entity being used.</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    /// <returns><see cref="IEnumerable" /> containing the entities.</returns>
    internal static IEnumerable<T> ToEntities<T>(DataTable dataTable)
    {
      var columns = dataTable.Columns.Cast<DataColumn>().ToList();

      var cancelTokenSource = new CancellationTokenSource();

      //TODO: Create timer that cancels the parallel operation upon timeout.
      var options = new ParallelOptions
      {
        CancellationToken = cancelTokenSource.Token,
        MaxDegreeOfParallelism = Settings.ThreadPoolLimit
      };

      var dataList = new ConcurrentDictionary<int, T>();

      Parallel.For(0, dataTable.Rows.Count, options, (i, loopState) =>
      {
        if(loopState.ShouldExitCurrentIteration)
        {
          return;
        }

        var dataRow = dataTable.Rows[i];

        T entity;

        try
        {
          entity = Activator.CreateInstance<T>();
        }
        catch(Exception ex)
        {
          throw new Exception(
            $"Default (parameterless) constructor does not exist for type: {typeof(T).Name}", ex);
        }

        //iterate through columns and set properties of the entity with the matching column values
        Populate(dataRow, entity, columns);

        //entity populated. Add it to the collection.
        dataList.TryAdd(i, entity);
      });

      return dataList.OrderBy(d => d.Key).Select(d => d.Value);
    }

    /// <summary>
    ///   Populates the entity using the specified <see cref="DataRow" />.
    /// </summary>
    /// <typeparam name="T">The type of entity being used.</typeparam>
    /// <param name="dataRow">The <see cref="DataRow" />.</param>
    /// <param name="entity">The entity.</param>
    /// <param name="columns">Column list.</param>
    private static void Populate<T>(DataRow dataRow, T entity, IList<DataColumn> columns)
    {
      for(var i = 0; i < columns.Count; i++)
      {
        try
        {
          var entityType = entity.GetType();
          var properties = RetrieveMatchingProperties(columns[i].ColumnName, entityType);

          if(properties.Count <= 0)
          {
            continue;
          }

          foreach(var pi in properties)
          {
            var childColumnMap = (ChildColumnMap[])Attribute.GetCustomAttributes(pi, typeof(ChildColumnMap));

            if(childColumnMap.Length <= 0)
            {
              var val = dataRow[i] is DBNull
                ? Utilities.GetDefault(pi.PropertyType)
                : Utilities.ChangeType(dataRow[i], pi);
              pi.SetValue(entity, val, null);
            }
            else
            {
              var attribute =
                childColumnMap.Single(m => m.Name.ToUpper().Equals(columns[i].ColumnName.ToUpper()));

              if(attribute == null)
              {
                continue;
              }

              try
              {
                var val = pi.GetValue(entity, null) ?? Activator.CreateInstance(pi.PropertyType);

                pi.SetValue(val,
                  dataRow[i] is DBNull
                    ? Utilities.GetDefault(pi.PropertyType)
                    : Utilities.ChangeType(dataRow[i], pi),
                  null);
              }
              catch(Exception ex)
              {
                throw new Exception(
                  $"Default (parameterless) constructor does not exist for type: {typeof(T).Name}", ex);
              }
            }
          }
        }
        catch(TypeConversionException tce)
        {
          throw new PopulationException(tce.PropertyName, tce.PropertyType, columns[i].ColumnName,
            dataRow[i].GetType(), dataRow[i],
            $"Property Population Failed! Property Name: {tce.PropertyName}, Property Type: {tce.PropertyType.Name}, Column Name: {columns[i].ColumnName}, Column Type: {dataRow[i].GetType().Name}",
            tce);
        }
        catch(Exception ex)
        {
          throw new Exception("PopulateModel.cs --> PopulateEntity<T> : Error while populating entity", ex);
        }
      }
    }

    private static List<PropertyInfo> RetrieveMatchingProperties(string columnName, Type entityType)
    {
      var objectCache = ObjectCache.Instance;

      if(objectCache.ContainsProperty(entityType, columnName))
      {
        return objectCache.RetrieveMatchingProperties(columnName, entityType);
      }

      var matches = new List<PropertyInfo>();
      var pi = entityType.GetProperty(columnName,
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);

      if(pi != null)
      {
        matches.Add(pi);

        objectCache.AddCacheObject(entityType, pi, Attribute.GetCustomAttributes(pi));
      }

      var properties = entityType.GetProperties();

      foreach(var propertyInfo in properties.Where(p => pi == null || p != pi))
      {
        if(objectCache.ContainsColumnNameAttribute(entityType, propertyInfo.Name)
           || Attribute.GetCustomAttribute(propertyInfo, typeof(EntityIgnore)) != null)
        {
          continue;
        }

        var attributes =
          Attribute.GetCustomAttributes(propertyInfo, typeof(ColumnName))
            .Where(a => ((ColumnName)a).Name == columnName)
            .ToArray();

        if(attributes.Length <= 0)
        {
          continue;
        }

        matches.Add(propertyInfo);
        objectCache.AddCacheObject(entityType, propertyInfo, attributes);
      }

      return matches;
    }

    private static PropertyInfo GetChildPropertyInfo<T>(T entity, string propertyName)
    {
      var entityType = entity.GetType();
      var properties = entityType.GetProperties();

      var pi = (from p in properties
                where p.Name.ToUpper().Equals(propertyName.ToUpper())
                select p).SingleOrDefault();

      return pi;
    }
  }
}
